package edu.cmu.arch.pipesandfilters.utils;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import edu.cmu.arch.pipesandfilters.frame.DataType;
import edu.cmu.arch.pipesandfilters.frame.Frame;
import edu.cmu.arch.pipesandfilters.frame.Variable;


/**
 * This class is a business helper that is used to convert a frame of byte data
 * into an easier to manipulate Java object (SingleFrame) and again convert
 * it back to raw bytes.
 * 
 * @author Kyle Benedict (jbenedic@andrew.cmu.edu)
 *
 */
public class FrameUtils {
	/**
	 * A buffer size sufficiently larger in bytes than a Frame
	 * Our frame, including the custom extra sixth field, would
	 * be 6 * 12, or 72 bytes in all. To make sure we're just always
	 * reading more than one frame, our buffer will be 20% more than
	 * that, or 87 bytes in all.
	 */
	public static final int BUFFER_SIZE = 87; 

	/**
	 * This static method is used to convert some number of bytes into a Frame.
	 * The exact size of the Frame may vary, which adds to the complexity of
	 * the solution.  If a full frame is not contained in the parameter stream,
	 * a null frame will be returned and the stream will be reset to its
	 * original position.  This is to allow more bytes to be added to the stream
	 * by the caller in order to attempt to create a Frame once more data is
	 * added to the stream. 
	 * @param ByteArrayInputStream bytes of the raw pipe data
	 * @return Frame
	 * @throws IllegalArgumentException and IOException 
	 */
	public static Frame frameFromStream(ByteArrayInputStream inputStream) throws IllegalArgumentException, IOException {		
		if (inputStream == null) {
			throw new IllegalArgumentException("InputStream is null");
		}
		if (inputStream.available() == 0) {
			throw new IllegalArgumentException("InputStream has no bytes available to read");
		}

		// Mark the stream so that we can revert to this location if
		// we do not yet have enough data to complete the frame.
		if (inputStream.markSupported()) {
			inputStream.mark(BUFFER_SIZE); // Change this to a static constant
		}		
		Frame frame = null;
		byte[] fourByteArr = new byte[4];
		int timeIdCount = 0;
		try {	
			while (true) {
				// Attempt to read a four byte array as the id
				int bytesRead = inputStream.read(fourByteArr);
				if (bytesRead == 4) {					
					int id = BitwiseManipulation.getIntegerFromBytes(fourByteArr);
					Variable variable = Variable.getVariableById(id);
					if (Variable.Time.equals(variable)) {		
						timeIdCount++;
					}
					if (frame == null && timeIdCount == 1) { 
						//start of frame found, create a new Frame object
						//Debug only, will remove
						frame = new Frame();
					}
					if (timeIdCount > 1) {
						// We read the Time ID which signals the start of
						// the next frame, roll back the stream to the
						// previous read and exit, returning the frame
						inputStream.reset();
						break;
					}					
					if (frame == null) {
						// throw an error, we've somehow read four bytes
						// but have yet to find the start of the frame
						throw new IllegalArgumentException("Corrupt data stream detected.  " +
						"Bytes have been read without the identification of the start of the frame.");
					}					
					// We successfully found a variable.  Next, get its data 
					// type in its number of bytes
					DataType dataType = variable.getDataType();
					int numBytes = dataType.getNumberOfBytes();
					byte[] byteArr = new byte[numBytes];					
					bytesRead = inputStream.read(byteArr);
					if (bytesRead == numBytes) {
						// We read the correct number of bytes, attempt to
						// convert it to the appropriate data type
						Number number = null;
						switch (dataType) {
						case Double: 
							number = BitwiseManipulation.getDoubleFromBytes(byteArr);
							break;							
						case Long: 
							number = BitwiseManipulation.getLongFromBytes(byteArr);
							break;							
						case Integer: 
							number = BitwiseManipulation.getIntegerFromBytes(byteArr);
							break;
						default:
							// throw an error, we've somehow read a portion
							// of the stream that does not convert to a valid
							// data type (currently Double, Long, or Integer).
							throw new IllegalArgumentException("Corrupt data stream detected.  " +	"DataType enum not located for value '" + String.valueOf(dataType) + "'.");
						}
						frame.putValue(variable, number);
						// Mark the stream so we can revert here if we find
						// the start of the next frame
						inputStream.mark(BUFFER_SIZE);
					}
				} else {					
					inputStream.mark(BUFFER_SIZE);
					break;
				}
			} 					
		} catch (IOException ioe) {
			// log the exception, and throw it back to caller
			System.out.println("IOException converting bytes to Frame:  " + ioe.getMessage());
			ioe.printStackTrace();
			throw ioe;
		} 		
		return frame;		
	}



	/**
	 * This static method is used to convert a Frame object to a byte stream.
	 * @param frame
	 * @return ByteArrayOutputStream
	 * @throws IllegalArgumentException and IOException
	 */
	public static ByteArrayOutputStream frameToStream(Frame frame) throws IllegalArgumentException, IOException {
		if (frame == null) {
			throw new IllegalArgumentException("Frame is null");
		}
		ByteArrayOutputStream oStream = new ByteArrayOutputStream();
		for (Variable v : Variable.values()) {
			Number n = frame.getValue(v);
			// Do not write null variables to the stream -- they weren't there
			// to begin with so don't put them in now.
			if (n != null) {
				try {
					oStream.write(BitwiseManipulation.getBytesFromInteger(v.getId()));
					DataType d = v.getDataType();
					switch (d) {
					case Double:
						oStream.write(BitwiseManipulation.getBytesFromDouble(n.doubleValue()));
						break;
					case Long:
						oStream.write(BitwiseManipulation.getBytesFromLong(n.longValue()));
						break;
					case Integer:
						oStream.write(BitwiseManipulation.getBytesFromInteger(n.intValue()));
						break;
					default:
						// throw an error, we've somehow created an invalid
						// enum, missing the reference to a DataType.  Since
						// this cannot happen, its more likely that if this
						// is ever triggered, someone added a new DataType
						// enum, and added it to the Variable enum, but 
						// didn't know to add it here during frame conversion
						// to bytes.
						throw new IllegalArgumentException("Corrupt data stream detected.  " +	"DataType enum not located for value '" + String.valueOf(d) + "'.");
					}
				} catch (IOException ioe) {
					// log the exception, and throw it back to caller
					System.out.println("IOException converting Frame to bytes:  " + ioe.getMessage());
					ioe.printStackTrace();
					throw ioe;					
				}
			}
		}
		return oStream;
	}


}
